Python

De computertaal Python werd ooorspronkelijk ontworpen als een open source computertaal die voor iedereen makkelijk te leren en te programmeren zou zijn. Dat verklaart de enorme populariteit ervan. Maar door die enorme populariteit is het in de loop der tijd toch ook een heel complexe taal geworden. Er zijn eindeloos veel boeken verschenen over Python. De inhoud van deze boeken wil ik niet herhalen. Deze pagina gaat ervanuit dat je met de eerste beginselen van de taal al kennisgemaakt hebt. Op deze webpagina verzamel ik allerlei weetjes die ik makkelijk wil kunnen terugvinden.

User input

Met de functie input() kun je iemand iets laten intypen. Wat de gebruiker intikt wordt altijd als 'string' geïnterpreteerd. Dat betekent dat Python de ingetypte tekst ziet als een opeenvolging van lettertekens zonder enige betekenis. Als je dus 12 intikt, dan ziet Python dat niet per se als een getal, maar als een één gevolgd door een twee. Mocht je willen dat wat ingetikt wordt anders wordt opgevat, moet je het ingetikte converteren naar het gewenste gegevenstype.

def main():
    x = input('x = ')
    print(type(x))
    if x.isnumeric():
        x = int(x)
        print(type(x))

if __name__ == '__main__':
    main()

Gegevenstypen

Python kent verschillende soorten gegevens. De meest bekende zijn:

letterscharactersstr()
gehele getallenintegersint()
drijvende-komma-getallenfloating point numbersfloat()
complexe getallencomplex numbers
waar of niet waarBooleans

In Python wordt geen decimale komma gebruikt, maar een decimale punt.

Een bestand aanmaken

Een bestand kun je met de volgende code aanmaken:

def main():
    file_object = open('outfile.csv', 'w')
    file_object.write("Hello\n")
    file_object.close()

if __name__ == '__main__':
    main()        

Een bestand inlezen

Voor mijn privé-programma's, vind ik csv-bestanden inlezen de gemakkelijkste vorm van invoer. Csv-bestanden kun je makkelijk aanmaken met Kladblok of Excel. Mijn voorkeur gaat uit naar de puntkomma als scheidingsteken. Het inlezen van een bestand kan in Python op verschillende manieren. De eerste manier is door een bestand te openen voor lezen, vervolgens het bestand te doorlopen met een for-loop. Bij een csv-bestand kun je in die for-loop elk record splitsen in verschillende velden. Na verwerking moet je het bestand weer te sluiten.

def main():
    file_object = open('myfile.csv', 'r')
    for line in file_object:
        print(line.strip('\n'))
        velden = line.split(';')
        for v in velden:
            print(v)
    file_object.close()
    
if __name__ == '__main__':
    main() 

Een andere manier om een bestand in te lezen gaat met behulp van een with-context. Daarbij hoef je de file niet te sluiten, want de context van het with-statement zorgt ervoor dat dat gebeurt :

import sys

def main():
    filename = 'MyFile.txt'
    try:
        with open(filename) as f_input:
            for line in f_input:
                line = line.strip('\n')
                print(line)
    except Exception as err:
        print('(1) err')
        print( err )
        print('(2) sys.exc_info()[0]')
        print( sys.exc_info()[0] )          # exception class
        print('(3) sys.exc_info()[1]')
        print( sys.exc_info()[1] )          # value
        print('(4) sys.exc_info()[2]')
        print( sys.exc_info()[2] )          # traceback object
        print('=====')         
if __name__ == '__main__':
    main()  

Als het bestand MyFile.txt niet bestaat, geeft het programma de volgende output:

(1) err
[Errno 2] No such file or directory: 'MyFile.txt'
(2) sys.exc_info()[0]
<class 'FileNotFoundError'>
(3) sys.exc_info()[1]
[Errno 2] No such file or directory: 'MyFile.txt'
(4) sys.exc_info()[2]
<traceback object at 0x000002C6B50EB200>
=====

Exceptions

Je kunt het afhandelen vvan de fout in bovenstaand programma wat eleganter laten verlopen door de de class die in bovenstaande foutmelding werd genoemd, afzonderlijk af te handelen:

import sys

def main():
    filename = 'MyFile.txt'
    try:
        with open(filename) as f_input:
            for line in f_input:
                line = line.strip('\n')
                print(line)
    except FileNotFoundError:
        print('Bestand ' + filename + ' is niet aanwezig')
    except Exception as err:
        print('(1) err')
        print( err )
        print('(2) sys.exc_info()[0]')
        print( sys.exc_info()[0] )          # exception class
        print('(3) sys.exc_info()[1]')
        print( sys.exc_info()[1] )          # value
        print('(4) sys.exc_info()[2]')
        print( sys.exc_info()[2] )          # traceback object
        print('=====')         
if __name__ == '__main__':
    main()  

Als je de html-file die gaat over CSS, als invoerbestand neemt, wordt het programma maar deels uitgevoerd. Het eindigt met de volgende regels:

            Als de viewport breed genoeg is, staan de verschillende blokken naast elkaar.
(1) err
'charmap' codec can't decode byte 0x9d in position 3699: character maps to 
(2) sys.exc_info()[0]

(3) sys.exc_info()[1]
'charmap' codec can't decode byte 0x9d in position 3699: character maps to 
(4) sys.exc_info()[2]
<traceback object at 0x000001FA93C32E80>
=====

Deze informatie is niet voldoende om te weten wat er aan de hand is. We breiden de code uit:

import sys, traceback

def main():
    filename = 'MyFile.txt'
    try:
        with open(filename) as f_input:
            for line in f_input:
                line = line.strip('\n')
                print(line)
    except FileNotFoundError:
        print('Bestand ' + filename + ' is niet aanwezig')
    except Exception as err:
        print('(1) err')
        print( err )
        print('(2) sys.exc_info()[0]')
        print( sys.exc_info()[0] )          # exception class
        print('(3) sys.exc_info()[1]')
        print( sys.exc_info()[1] )          # value
        print('(4) sys.exc_info()[2]')
        print( sys.exc_info()[2] )          # traceback object
        print('(5) err.object')
        print( err.object )
        print('(6) traceback.print_exception(err)')
        traceback.print_exception(err)
        print('(7) traceback.print_tb(err)')
        traceback.print_tb(err)             # traceback
        print('=====')         
if __name__ == '__main__':
    main()  

De output wordt:

            Als de viewport breed genoeg is, staan de verschillende blokken naast elkaar.
(1) err
'charmap' codec can't decode byte 0x9d in position 3699: character maps to <undefined>
(2) sys.exc_info()[0]
<class 'UnicodeDecodeError'>
(3) sys.exc_info()[1]
'charmap' codec can't decode byte 0x9d in position 3699: character maps to <undefined>
(4) sys.exc_info()[2]
<traceback object at 0x0000022A10433940>
(5) err.object
Squeezed text (64 lines).
(6) traceback.print_exception(err)
Traceback (most recent call last):
  File "E:\Site\Site20240516\prive\ict\extra\test0001.py", line 7, in main
    for line in f_input:
  File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\encodings\cp1252.py", line 23, in decode
    return codecs.charmap_decode(input,self.errors,decoding_table)[0]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
UnicodeDecodeError: 'charmap' codec can't decode byte 0x9d in position 3699: character maps to <undefined>
(7) traceback.print_tb(err)
Traceback (most recent call last):
  File "E:\Site\Site20240516\prive\ict\extra\test0001.py", line 7, in main
    for line in f_input:
  File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\encodings\cp1252.py", line 23, in decode
    return codecs.charmap_decode(input,self.errors,decoding_table)[0]
UnicodeDecodeError: 'charmap' codec can't decode byte 0x9d in position 3699: character maps to <undefined>

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "E:\Site\Site20240516\prive\ict\extra\test0001.py", line 29, in <module>
    main()
  File "E:\Site\Site20240516\prive\ict\extra\test0001.py", line 26, in main
    traceback.print_tb(err)             # traceback
  File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\traceback.py", line 55, in print_tb
    print_list(extract_tb(tb, limit=limit), file=file)
  File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\traceback.py", line 74, in extract_tb
    return StackSummary._extract_from_extended_frame_gen(
  File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\traceback.py", line 418, in _extract_from_extended_frame_gen
    for f, (lineno, end_lineno, colno, end_colno) in frame_gen:
  File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\traceback.py", line 355, in _walk_tb_with_full_positions
    positions = _get_code_position(tb.tb_frame.f_code, tb.tb_lasti)
AttributeError: 'UnicodeDecodeError' object has no attribute 'tb_frame'

Merk op dat het statement "print('=====')" niet wordt uitgevoerd. Het programma is na het afdrukken van de foutmeldingen afgebroken.

Deze foutmelding is te complex voor mij. Ik ga op zoek op Internet, en ontdek dat anderen al meer dan 6 jaar eerder met deze foutmelding worstelden. Als mogelijke oorzaak wordt geopperd dat het bestand niet met utf-8 is aangemaakt. Ik heb de input-file aangemaakt met behulp van het Kladblok-programma in Windows. Windows schijnt niet met utf-8 te werken. De suggesties die op stackoverklow.com worden gegeven, zijn:

De eerste suggestie leidt tot:

import sys, traceback

def main():
    filename = 'MyFile.txt'
    tel = 0
    try:
        with open(filename, 'rb') as f_input:
            for b_line in f_input:
                line = b_line.decode('utf-8')
                line = line.strip('\n')
                print(line)
    except FileNotFoundError:
        print('Bestand ' + filename + ' is niet aanwezig')
    except Exception as err:
        print('(1) err')
        print( err )
        print('(2) sys.exc_info()[0]')
        print( sys.exc_info()[0] )          # exception class
        print('(3) sys.exc_info()[1]')
        print( sys.exc_info()[1] )          # value
        print('(4) sys.exc_info()[2]')
        print( sys.exc_info()[2] )          # traceback object
        print('(5) err.object')
        print( err.object )
        print('(6) traceback.print_exception(err)')
        traceback.print_exception(err)
        print('(7) traceback.print_tb(err)')
        traceback.print_tb(err)             # traceback
        print('=====')         
if __name__ == '__main__':
    main()  

While-statement

Het volgende programma telt een aantal getallen bij elkaar op.

def main():
    numbers = [1, 2, 3, 5, 7, 11, 
               13, 17, 19, 23, 29]
    quantity = len(numbers)
    total = 0
    i = 0
    while i < quantity:
        total = total + numbers[i]
        i = i + 1
    print(quantity)
    print(total)

if __name__ == '__main__':
    main()








In het while-statement wordt de voorwaarde 'i < quantity' getest. Er zijn twee plaatsen die voorafgaand aan deze test kunnen worden uitgevoerd. Beide zorgen ervoor dat de voorwaarde het juiste resultaat oplevert. Dat zijn het statement 'i = 0' voorafgaand aan het while-statement en het statement 'i = i + 1' aan het einde van block dat wordt uitgevoerd als de voorwaarde True oplevert.

Variaties op het inlezen van een bestand

Ik vind het leuk om af en toe op een primitieve manier te programmeren. Met primitief bedoel ik dat enkel gebruik wordt gemaakt van if- en while-statements. Ik gebruik dus geen for-statements, en geen object-georiënteerde technieken. Dus geen classes. Een soort heimwee naar de taal Algol, de eerste computertaal waarin ik geprogrammeerd heb.

def lees_bestand():
    file_name = 'een_csv_bestand.csv'
    file_object = open(file_name)
    line = file_object.readline()
    print(line)
    while line != '':
        line = file_object.readline()
        print(line)
    file_object.close()
    
lees_bestand()

Als het input-bestand de volgende inhoud heeft

David;20240112;35.00
Jim;20240123;44.00
Ken;20240125;32.81
John;20240131;4.33

David;20240204;35.00
John;20240205;21.61
Ken;20240221;12.32
David;20240228;16.37

dan ziet de output er als volgt uit:

David;20240112;35.00

Jim;20240123;44.00

Ken;20240125;32.81

John;20240131;4.33



David;20240204;35.00

John;20240205;21.61

Ken;20240221;12.32

David;20240228;16.37


Ten opzichte van de input, worden er lege regels toegevoegd. Dat komt omdat er in het input-bestand aan het einde van elke regel een onzichtbaar nieuwe-regel-teken staat.
Als ik het bestand inlees als binary-bestand, met het volgende programma

input_file = open('py0051.csv', 'rb')
inhoud_bestand = input_file.read()
input_file.close
print(inhoud_bestand)

dan krijg ik op mijn Windows-computer als output
b'David;20240112;35.00\r\nJim;20240123;44.00\r\nKen;20240125;32.81\r\nJohn;20240131;4.33\r\n\r\nDavid;20240204;35.00\r\nJohn;20240205;21.61\r\nKen;20240221;12.32\r\nDavid;20240228;16.37\r\n'

De onzichtbare tekens \r en \n worden nu getoond. De tekenreeks begint met b gevolgd door een enkele quote (') en eindigt met een enkele quote. Dat wil zeggen dat het om een binary-weergave gaat. We gaan achterhalen welke binaire code er verscholen zit achter de tekens \r en \n.

x = b'\r'
print( ord(x) )         # 13
print( hex(ord(x)) )    # 0xd

x = b'\n'
print( ord(x) )         # 10
print( hex(ord(x)) )    # 0xa

Op internet kun je met de zoekterm 'ascii' vinden waar \r en \n voor staan:

 deci-
maal
hexa-
deci-
maal
 symbool  engels nederlands
 \r 13DCR carriage return  ga naar het begin van de regel 
 \n 10ALF line feed  ga naar de volgende regel 

Denk hierbij aan een ouderwetse mechanische typemachine waarbij je het papier omhoog moest draaien om naar de volgende regel te gaan (LF) en vervolgens de rol met papier naar rechts moest schuiven om aan het begin van de regel te gaan typen (CR). De afkorting ascii staat voor 'American Standard Code for Information Interchange'.

Hexadecimaal betekent 16-tallig. Dat betekent dat je een getal niet met de cijfers 0 t/m 9 weergeeft, maar ook A, B, C, D, E en F als cijfers beschouwt, met A=10, B=11, C=12, D=13, E=14, F =15. Dus: het decimale getal 13 komt overeen met het hexadecimale getal D, en het decimale getal 10 komt overeen met het hexadecimale getal A.

Hexadecimale en decimale representatie

def main():
    x = 'a'

    print( ord(x) )                     # 97
    print( x.encode('utf-8') )          # b'a'
    print( bytes(x, 'utf-8') )          # b'a'
    
    print( hex(ord(x)) )                # 0x61
    print( x.encode('utf-8').hex() )    # 61
    print( bytes(x, 'utf-8').hex() )    # 61

    n = 97

    print( chr(n) )                     # a

    b = b'\x61'

    print( str(b, 'utf-8') )            # a
    print( b.decode('utf-8') )          # a

if __name__ == '__main__':
    main()

Classes

Types

Gehele getallen

In Python kun je getallen gewoon optellen. Het programma

print(2 + 3)   # 5

heeft als output gewoon 5.

Variabelen

Maar als je in plaats van getallen letters intikt, bijvoorbeeld

print(abc + def)

dan interpreteert Python abc en def als namen van vaiabelen of als verboden tekencombinaties. Als die variabelen in het voorgaande niet dedefinieerd zijn, geeft Python een foutmelding. In dit geval wordt een syntax-error gemeld, omdat def een tekencombinatie is die je niet mag gebruiken, omdat def al gebruikt wordt om functies en methoden te definiëren.

Tekenreeksen

Wel kun je tekenreeksen abc en def 'optellen' als je aanhalingstekens om abc en def zet.

print('abc' + 'def')      # 'abcdef'

Het resultaat van dat 'optellen' is dat Python de tekenreeksen achter elkaar zet. Dat betekent dat het optellen van gehele getallen iets anders is dan optellen van tekenreeksen.

Verschillende betekenissen van +, −, * en /

Het programma

print( type(2) )           # <class 'int'>
print( type(3) )           # <class 'int'>
print( type(5) )           # <class 'int'>
print( type('abc') )       # <class 'str'>
print( type('def') )       # <class 'str'>

geeft aan dat 2, 3 en 5 van het type int zijn. int is een afkorting voor integer, dat is een geheel getal; str is de afkorting voor string, wat staat voor tekenreeks. Vandaar dat 2 + 3 een andere uitkomst geeft dan '2' + '3':

print(2 + 3)          # 5
print('2' + '3')      # '23'

Ook andere bewerkingen als aftrekken, delen en vermenigvuldigen, werken bij verschillende gegevenstypen net iets anders.

Drijvende-komma-getallen

Als je gaat delen, bijvoorbeeld

print( 5 / 2 )         # 2.5
print( 6 / 2 )         # 3.0

dan zie je dat de uitkomst een getal is waarin een decimale punt voorkomt, ook als de uitkomst van de deling een geheel getal is. Als je het type van zo'n getal met decimale komma opvraagt,

print( type(2.5) )           # <class 'float'>
print( type(3.0) )           # <class 'float'>

dan blijken 2.5 en 3.0 het type float te hebben, wat wil zeggen dat het drijvende-komma-getallen zijn. Drijvende-komma-getallen worden op een speciale manier opgeteld. Dat het een andere manier van optellen is als bij gewone gehele getallen, kun je zien in het volgende programma:

print( 1 + 1 + 1 - 3) / 10 )        # 0.0
print( 0.1 + 0.1 + 0.1 - 0.3) )     # 5.551115123125783e-17

e-17 staat voor 10-17. Beide optellingen zouden gelijk moeten zijn aan 0, maar door afrondingsverschillen is het antwoord bij het optellen van drijvende-komma-getallen bijna nul.

print( 1e-0 )        # 1.0
print( 1e-1 )        # 0.1
print( 1e-2 )        # 0.01
print( 1e-3 )        # 0.001

Het optellen en delen gaat bij drijvende-komma-getallen dus op een andere manier dan bij gehele getallen.

Decimale getallen

Python kent ook decimale getallen.

import decimal
print( decimal.Decimal(0.1) + 
       decimal.Decimal(0.1) + 
       decimal.Decimal(0.1) - 
       decimal.Decimal(0.3)   )   # Decimal(0.0)
print( type( decimal.Decimal(5.43) ) ) # <class 'decimal.Decimal'>

Bij decimale getallen kun je de precisie instellen.

import decimal
print( decimal.Decimal(1) / decimal.Decimal(7) )
    # 0.1428571428571428571428571429
decimal.getcontext().prec = 4
print( decimal.Decimal(1) / decimal.Decimal(7) )
    # 0.1429
print( type( decimal.Decimal(5.43) ) ) # <class 'decimal.Decimal'>

Complexe getallen

In Python worden getallen gevolgd door een j geïnterpreteerd als het imaginaire deel van een complex getal.

print( 2j + 3j )        # 5j
print( 2.0j + 3.0j )    # 5j
print( 2.1j + 3.2j )    # 5.300000000000001j

In de wiskunde wordt het complexe getal (0, 1) meestal aangegeven met de letter i, maar in de elektrotechniek, waar i gebruikt wordt voor stroomsterkte, wordt meestal de letter j gebruikt. Python volgt de notatie die gebruikelijk is in de elektrotechniek.

print( type( 5j ) )     # <class 'complex'>
print( type( 5.3j ) )   # <class 'complex'>

Breuken

Python kent ook breuken.

from fractions import Fraction
x = Fraction(3, 5)
y = Fraction(2, 5)
print(x)                  # 3/5
print(y)                  # 2/5
print(x + y)              # 1
print(x - y)              # 1/5
print( type(x) ) # <class 'fractions.Fraction'> print( type(x + y) ) # <class 'fractions.Fraction'>
print( Fraction('0.25') ) # Fraction(1, 4) print( Fraction('0.25') + Fraction('1.25') ) # Fraction(3, 2)

Voor breuken bestaan verschillende conversie-functies.

from fractions import Fraction
print(            (3.5).as_integer_ratio() )    # 7, 2
print( Fraction( *(3.5).as_integer_ratio() ) )  # 7/2
print( Fraction.from_float(1.75) ) # Fraction(7, 4)

Classes maken

Een lege class maken

We hebben hierboven gezien dat je van elke variabele de class kunt opvragen met behulp van het statement type(). Naast de classes die standaard in Python aanwezig zijn, kun je ook zelf classes maken. Dat gaat als volgt:

class MyClass:
    pass

obj = MyClass()     
print(MyClass)          # 
print(obj)              # <__main__.MyClass object at 0x0000024D8ED0A900>
print(type(MyClass))    # 
print(type(obj))        # 

Met het commando 'class MyClass' maak je een nieuwe class. Met 'pass' geef je aan dat het een class betreft, waar je dus nog bijna niets mee kunt doen. Het enige wat je ermee kunt doen is een object maken. Met 'obj = MyClass()' maak je een object met de naam obj van class MyClass. Vervolgens blijken MyClass en obj te bestaan, want je kunt ze printen. Vervolgens wordt met type gekeken wat de classes zijn van MyClass en obj. MyClass blijkt van de class 'type' te zijn.
Er zijn al wel een hoop methoden aanwezig. De methoden bij een object kun je opvragen met het commando dir().

class MyClass:
    pass

obj = MyClass() 
print('***** Methoden van MyClass *****')       
print(dir(MyClass))
print('***** Methoden van obj *****')       
print(dir(obj))

De output is:

***** Methoden van MyClass *****
['__class__', '__delattr__', '__dict__', '__dir__',
'__doc__', '__eq__', '__firstlineno__', '__format__',
'__ge__', '__getattribute__', '__getstate__', '__gt__',
'__hash__', '__init__', '__init_subclass__', '__le__',
'__lt__', '__module__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__static_attributes__', '__str__', '__subclasshook__',
'__weakref__'] ***** Methoden van obj ***** ['__class__', '__delattr__', '__dict__', '__dir__',
'__doc__', '__eq__', '__firstlineno__', '__format__',
'__ge__', '__getattribute__', '__getstate__', '__gt__',
'__hash__', '__init__', '__init_subclass__', '__le__',
'__lt__', '__module__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__static_attributes__', '__str__', '__subclasshook__',
'__weakref__']

instance-variables

Er miljoenen drijvende-komma-getallen denkbaar. Anders gezegd: Op basis van een class (bijvoorbeeld drijvende-komma-getallen) kun je een veelheid aan verschillende objecten (miljoenen drijvende-komma-getallen) creëren. Voorbeeld:

a = 2.3
b = 6.53
c = 21.43
pi = 3.14159
print( type(a),type(b)), type(c)), type(pi))

De getoonde variabelen a, b,c en pi nooemen we instance-variables van de class float. genoemd. Soms wordt dat naar het Nederlands vertaald vertaald met instantie-variabelen. Een instantie-variabele kan bij elk object weer een andere waarde hebben. Als je zelf een class maakt met het class-commando, kun je zo'n instance-varable direct aan een object koppelen. In onderstaand voorbeeld wordt de instance-variable straal gekoppeld aan de objecten cirkel_1 en cirkel_2::

class Cirkel:
    pass

cirkel_1 = Cirkel()
cirkel_1.straal = 4.774653    
cirkel_2 = Cirkel()
cirkel_2.straal = 3.183102    

print( 2 * 3.14159 * cirkel_1.straal)
                 # 30.000004236539997
print( 2 * 3.14159 * cirkel_2.straal)
                 # 20.00000282436

print( type( Cirkel ) )   
                    # <class 'type'>
print( type( cirkel_1 ) )
         # <class '__main__.Cirkel'>
print( type( cirkel_2 ) ) 
         # <class '__main__.Cirkel'>

Een nieuwe class maken met __init__()

Wat je vaker tegenkomt, is dat instance_variables via een method __init__() in de class-definitie aan het object wordt gekoppeld. Ook een berekening kan in de class-definitie worden opgenomen. Zo'n berekening wordt als functie aan de class gekoppeld. Een functie die aan een class is gekoppeld, wordt een method genoemd.

class Cirkel:
    def __init__(self, straal):
        self.straal = straal

    def omtrek(self):
        return  2 * 3.14159 * self.straal

cirkel_1 = Cirkel(4.774653)
cirkel_2 = Cirkel(3.183102)

print( cirkel_1.omtrek() )
                 # 30.000004236539997
print( cirkel_2.omtrek() )
                 # 20.00000282436

self verwijst naar het object. Hoewel __init__() twee variabelen kent, namelijk self en straal, hoef je in de commando's cirkel_1 = Cirkel(4.774653) en cirkel_2 = Cirkel(3.183102) bij class Cirkel maar één parameter (straal) mee te geven.

Class-variables

Een class-variabele is gekoppeld aan een class. Een class-variable is ook benaderbaar als er nog geen enkel object van de class is aangemaakt. Een class-variabele kan benaderd worden vauit alle objecten die op basis van die class zijn gemaakt.

import math

class Cirkel:
    pi = math.pi

    def __init__(self, straal):
        self.straal = straal

    def omtrek(self):
        return  2 * Cirkel.pi * self.straal

print( Cirkel.pi )      # 3.141592653589793

cirkel_1 = Cirkel(4.774653)
cirkel_2 = Cirkel(3.183102)

print( cirkel_1.pi )    # 3.141592653589793
print( cirkel_1.straal )         # 4.774653
print( cirkel_1.omtrek() )
                        # 30.00002957648093

print( cirkel_2.pi )    # 3.141592653589793
print( cirkel_2.straal )         # 3.183102
print( cirkel_2.omtrek() )
                       # 20.000019717653956

Alle objecten van een class doorlopen

Om alle objecten van een class te doorlopen, Maak je eerst een lege list als class_variabele aan. Als je een nieuw object aanmaakt, voeg je in het daarbij behorende __init__()-commando een verwijzing naar dat nieuwe object toe aan die list. De list bevat dan uiteindelijk verwijzingen naar alle eerder aangemaakte objecten. In het volgende voorbeeld zijn de objecten cirkels, die alle behoren tot de class Cirkel. In de volgende code gebruiken we self.__class__ voor de class waartoe de toe te voegen cirkel behoort.

import math

class Cirkel:
    pi = math.pi
    alle_cirkels = []

    def __init__(self, straal):
        self.straal = straal
        self.__class__.alle_cirkels.append(self)

print( Cirkel.alle_cirkels )
cirkel_1 = Cirkel(1)
print( Cirkel.alle_cirkels )
cirkel_2 = Cirkel(2)
print( Cirkel.alle_cirkels )
cirkel_3 = Cirkel(3)
print( Cirkel.alle_cirkels )

Static methods staan los van de class waar ze bij zijn ondergebracht

Je kunt bijvoorbeeld de som van de oppervlakten van de cirkels berekenen. In onderstaand voorbeeld wordt gebruik gemaakt van @staticmethod. Dat is een decorator die aangeeft dat de method die erop volgt, totaal_oppervlakte(), weliswaar is ondergebracht bij de class Cirkel, maar eigenlijk niets met die class te maken heeft. Een static method is aanwezig in een class omdat het op een of andere manier logisch is om de method daar onder te brengen. Er is wel een zekere functionaliteit die te maken heeft met de class, maar er hoeven geen objecten van de class aanwezig te zijn om de method uit te voeren. Een static method heeft geen parameter self of cls. Een static method is een method die gekoppeld is aan de class, maar niet aan een enkel object van de class. Vanuit een static method kunnen de variabelen die gedefinieerd zijn in de class, niet gewijzigd worden.

import math

class Cirkel:
    pi = math.pi
    alle_cirkels = []

    def __init__(self, straal):
        self.straal = straal
        self.__class__.alle_cirkels.append(self)

    def oppervlakte(self):
        return self.__class__.pi * self.straal * self.straal

    @staticmethod
    def totaal_oppervlakte():
        totaal = 0
        for c in Cirkel.alle_cirkels:
            totaal += c.oppervlakte()
        return totaal

cirkel_1 = Cirkel(1)
cirkel_2 = Cirkel(2)
cirkel_3 = Cirkel(3)
print( Cirkel.totaal_oppervlakte() )   # 43.982297150257104

Je roept een static method normaliter uit vanuit de class, niet vanuit een object. In het voorbeeld hierboven: Cirkel.totaal_oppervlakte(), en niet: cirkel_1.totaal_oppervlakte().

Class-methods

Class-methods zijn methods die niet aan een object zijn gekoppeld, maar aan een class. Het verschil met static methods houdt in dat (1) class-methods wel class_variabelen kunnen wijzigen en (2) in een class-method als eerste parameter een verwijzing naar de class zelf is opgenomen. Deze parameter wordt doorgaans niet self genoemd, maar cls. Een class-method moet worden voorafgegaan door de decorator @classmethod.

import math

class Cirkel:
    pi = math.pi
    alle_cirkels = []

    def __init__(self, straal):
        self.straal = straal
        self.__class__.alle_cirkels.append(self)

    def oppervlakte(self):
        return self.__class__.pi * self.straal * self.straal

    @classmethod
    def totaal_oppervlakte(cls):
        totaal = 0
        for c in Cirkel.alle_cirkels:
            totaal += c.oppervlakte()
        return totaal

print( Cirkel.totaal_oppervlakte() )

cirkel_1 = Cirkel(1)
cirkel_2 = Cirkel(2)
cirkel_3 = Cirkel(3)
print( Cirkel.totaal_oppervlakte() )

print('===')
print( cirkel_1.oppervlakte() )
print( cirkel_1.totaal_oppervlakte() )

Om de oppervlakte van cirkel één te weten te komen, moet je method cirkel_1.oppervlakte() uitvoeren, en niet cirkel_1.totaal_oppervlakte(), omdat totaal_oppervlakte een class-method is en de oppervlakte van alle aanwezige cirkels berekent, niet alleen die van cikel_1.

Properties

Geometrische vormen, zoals een cirkel, een rechthoek of een vierkant, hebben allemaal eeen oppervlakte. Om die oppervlaktes te berekenen, bestaan er formules. Oppervlakte kun je beschouwen als een eigenschap die bij de vorm hoort, of als het resultaat van een formule.
In onderstaand programma is de oppervlakte het resultaat van een method:

class Rechthoek:
    def __init__(self, hoogte, breedte):
        self.hoogte = hoogte
        self.breedte = breedte

    def oppervlakte(self):
        return self.hoogte * self.breedte

def main():
    rechthoekje = Rechthoek(2, 4)
    print( rechthoekje.oppervlakte() )     # 8

if __name__ == '__main__':
    main()

We gaan nu een aantal wijzigingen doorvoeren. (1) Allereerst vervangen we self.hoogte door self._hoogte en self.breedte door self._breedte. We plaatsen dus een underscore voor de variabele_namen. Hiermee geef je aan dat het niet is toegestaan dat deze variabelen van buitenaf worden gewijzigd. Met 'van buitenaf' wordt dan bedoeld 'van buiten de class-definitie'. (2) Vervolgens plaatsen we een regel met de tekst '@property' direct voorafgaand aan de regel 'def oppervlakte(self):'. (3) Als laatste vervangen we de aanroep 'rechthoekje.oppervlakte()' door 'rechthoekje.oppervlakte', d.w.z. dat we in de oproep de tekens () weglaten

class Rechthoek:
    def __init__(self, hoogte, breedte):
        self._hoogte = hoogte
        self._breedte = breedte

    @property
    def oppervlakte(self):
        return self._hoogte * self._breedte

def main():
    rechthoekje = Rechthoek(2, 4)
    print( rechthoekje.oppervlakte )     # 8

if __name__ == '__main__':
    main()

Met de decorator @property kun je dus wat door een method wordt berekend opvatten als een eigenschap van een object, dat niet zomaar gewijzigd mag worden. Wijziging mag alleen plaatsvinden vanuit een method van de class zelf.

class Rechthoek:
    def __init__(self, hoogte, breedte):
        self._hoogte = hoogte
        self._breedte = breedte

    @property
    def oppervlakte(self):
        return self._hoogte * self._breedte

def main():
    rechthoekje = Rechthoek(2, 4)
    print( rechthoekje.oppervlakte )     # 8

    rechthoekje.oppervlakte = 3    

if __name__ == '__main__':
    main()

heeft als output

8
Traceback (most recent call last): File "xxx.py", line 17, in main() File "xxx.py", line 14, in main rechthoekje.oppervlakte = 3 AttributeError: property 'oppervlakte' of 'Rechthoek' object has no setter

Setters

De conversie-formules voor graden Celsius en graden Fahrenheit zijn:

f = c * 9 / 5 + 32
c = (f - 32) * 5 / 9

We maken een class Temperatuur, waarin we de temperatuur opslaan in graden Celsius.

class Temperatuur:
    def __init__(self):
        self._celsius = 0

    @property
    def celsius_naar_fahrenheit(self):
        return self._celsius * 9 / 5 + 32

def main():
    temp = Temperatuur()
    print( temp._celsius , '\u00B0C')                   # 0 °C
    print( temp.celsius_naar_fahrenheit , '\u00B0F' )   # 32 °F

if __name__ == '__main__':
    main()

Omdat we ook andere temperaturen dan 0 °C willen kunnen weergeven, voegen we een setter toe aan de class Temperatuur.

class Temperatuur:
    def __init__(self):
        self._celsius = 0

    @property
    def celsius_naar_fahrenheit(self):
        return self._celsius * 9 / 5 + 32

    @celsius_naar_fahrenheit.setter
    def celsius_naar_fahrenheit(self, nieuwe_celsius):
        return nieuwe_celsius  * 9 / 5 + 32

def main():
    temp = Temperatuur()
    print( temp._celsius , '\u00B0C')                   # 0 °C
    print( temp.celsius_naar_fahrenheit , '\u00B0F' )   # 32 °F

    temp._celsius = 100

    print( temp._celsius , '\u00B0C')                   # 100 °C
    print( temp.celsius_naar_fahrenheit , '\u00B0F' )   # 212 °F

if __name__ == '__main__':
    main()

Vanuit functie main(), die niet gedefineerd is binnen de class Temperatuur, kunnen we toch de waarde van _celsius in object temp wijzigen.
Als we de naam celsius_naar_fahrenheit wijzigen in _fahrenheit, dan lijkt setter _fahrenheit op een variabele, die meeverandert met _celsius.

class Temperatuur:
    def __init__(self):
        self._celsius = 0

    @property
    def _fahrenheit(self):
        return self._celsius * 9 / 5 + 32

    @_fahrenheit.setter
    def _fahrenheit(self, nieuwe_celsius):
        return nieuwe_celsius  * 9 / 5 + 32

def main():
    temp = Temperatuur()
    print( temp._celsius , '\u00B0C')       # 0 °C
    print( temp._fahrenheit , '\u00B0F' )   # 32 °F

    temp._celsius = 100

    print( temp._celsius , '\u00B0C')       # 100 °C
    print( temp._fahrenheit , '\u00B0F' )   # 212 °F

if __name__ == '__main__':
    main()

Overigens, de regel 'return nieuwe_celsius * 9 / 5 + 32' mag je vervangen door 'self._fahrenheit = nieuwe_celsius * 9 / 5 + 32'.

Overerving ( Inheritance )

Het 'Liskov substitutie principe' betekent dat, als je super-class hebt waarvan een sub-class worrdt afgeleid, de sub-class dezelfde interface en implementatie moet erven van de super-class en dat objecten van de sub-class de objecten van de super-class kunnen vervangen. Anders gezegd: Het 'Liskov substitutie principe' zegt dat een afgeleid object - laten we dat Derived noemen - die erft van class - welke we Base noemen - het Base-object moet kunnen vervangen zonder de gewenste eigenschappen van een programma te veranderen.
Een voorbeeld: Veronderstel dat je denkt dat een vierkant een variant is van een rechthoek, en dat je de class Vierkant afleidt van de class Rechthoek. De eigenschap waar we dan onze aandacht op richten is oppervlakte, die is ondergebracht in de Base-class Rechthoek. In het programma hieronder ontstaat een fout, als je gebruik gaat maken van de functie resize(). De functie resize() kan gebruikt worden voor een object van de class Rechthoek(), maar niet voor een object van de class Vierkant.

class Rechthoek:
    def __init__(self, hoogte, breedte):
        self._hoogte = hoogte
        self._breedte = breedte

    @property
    def oppervlakte(self):
        return self._hoogte * self._breedte

    def resize(self, nieuwe_hoogte, nieuwe_breedte):
        self._hoogte = nieuwe_hoogte
        self._breedte = nieuwe_breedte

class Vierkant(Rechthoek):
    def __init__(self, zijde):
        super().__init__(zijde, zijde)

def main():
    rechthoekje = Rechthoek(2, 4)
    print( rechthoekje.oppervlakte )     # 8
    
    vierkantje = Vierkant(2)
    print( vierkantje.oppervlakte )      # 4

    rechthoekje.resize(3, 5)
    print( rechthoekje.oppervlakte )     # 15
    
    vierkantje.resize(3, 5)
    print( vierkantje.oppervlakte )      # 15     ????

if __name__ == '__main__':
    main()

De oorzaak van het feit dat je resize() niet kunt gebruiken voor een object van class Vierkant, is dat Rechthoek twee parameters vereist (hoogte en breedte) en Vierkant maar één (zijde). Vierkant heeft dus een andere interface als Rechthoek. Hoewel je een vierkant als een rechthoek kunt opvatten, kun je de class Vierkant beter niet programmeren als een subclass van de class Rechthoek, omdat dat heel gemakkelijk tot fouten kan leiden.

Een bestaande class uitbreiden met __new__()

Om iets zinnigs te kunnen doen met een class, kun je verschillende dingen doen. Eén daarvan is dat je een class kunt baseren op een andere class. Hieronder volgt een voorbeeld, waarin een eenheid aan de de class float wordt toegevoegd.

class FloatUnit(float):
    def __new__(cls, value, unit):
        instance = super().__new__(cls, value)
        instance.unit = unit
        instance.value = value
        return instance

    def __repr__(cls):
        return str(cls.value) + ' ' + str(cls.unit)

afstand = FloatUnit(1000, 'km')
print(afstand)             # 1000 km
print(type(afstand))       # <class '__main__.FloatUnit'>

De output van dit programma is:

1000 km
<class '__main__.FloatUnit'>

Je definieert eerst een methode __new__() die bij de class FloatUnit hoort. Deze wordt uitgevoerd onmiddellijk na het maken van de lege class. De methode super().__new__() hoort bij de class float. Deze methode zorgt ervoor, dat de class FloatUnit een attribuut met de naam instance krijgt, die een blauwdruk is voor een een float-waarde. In de daarop volgende commando's worden twee attributen aan deze blauwdruk toegevoegd, namelijk een waarde (value) en een eenheid (unit). In het return-commando wordt de attribuut instance gekoppeld aan de class FloatUnit. In dit voorbeeld volgt het print-commando wat je in de methode __repr__() hebt gedefinieerd.
In het volgende voorbeeld wordt naast __repr__() ook de methode __str__() gedefinieerd. Als __str__() gedefinieerd is, volgt print() de methode __str__() en niet de methode __repr__().

import decimal

class AmountOfMoney(decimal.Decimal):
    def __new__(cls, value, unit, sign):
        instance = super().__new__(cls, value)
        instance.unit = unit
        instance.value = value
        instance.sign = sign
        return instance

    def __str__(cls):
        return str(cls.sign) + ' ' + str(cls.value)
    
    def __repr__(cls):
        return str( cls.value) + ' ' + str(cls.unit)

geldbedrag = AmountOfMoney(1000, 'euro', '\N{EURO SIGN}')
print(type(geldbedrag))       # <class '__main__.AmountOfMoney'>
print(geldbedrag)             # € 1000
print(geldbedrag.__str__())   # € 1000
print(geldbedrag.__repr__())  # 1000 euro

Json

Json-standaard

Standaard JSON kent (1) geen commentaar, (2) geen komma's waarna niets volgt en (3) geen enkele aanhalingstekens bij strings.
Het omzetten van gegevens naar het JSON-formaat wordt serialization genoemd. Het tegenovergestelde proces, deserialization, houdt in dat gegevens in JSON-formaat wordt omgezet in een in Python-gegevenstype.

Van Python naar Json ( json.dumps )

De volgende Python-code zet de python-gegevenstypen om in strings met json-code.

import json

def python_to_json():
    print(json.dumps({"naam": "Jan", "leeftijd": 31})) # dictionary -> json-object
    print(json.dumps(["appel", "banaan"]))             # list       -> json-array
    print(json.dumps(("appel", "banaan")))             # tuple      -> json-array
    print(json.dumps("hallo"))                         # string     -> json-string
    print(json.dumps(56))                              # integer    -> json-number
    print(json.dumps(41.87))                           # float      -> json-number
    print(json.dumps(True))                            # True       -> json-true
    print(json.dumps(False))                           # False      -> json-false
    print(json.dumps(None))                            # None       -> json-null

python_to_json()

Het programma heeft als output:

{"naam": "Jan", "leeftijd": 31}
["appel", "banaan"]
["appel", "banaan"]
"hallo"
56
41.87
true
false
null

Van Json naar Python ( json.loads )

De volgende Python-code zet json-gegevens om naar python-gegevenstypen .

import json

def json_to_python():
    print(json.loads('{"naam": "Jan", "leeftijd": 31}')) # json-object -> dictionary
    print(json.loads('["appel", "banaan"]'))             # json-array  -> list
    print(json.loads('"hallo"'))                         # json-string -> string
    print(json.loads('56'))                              # json-number -> integer
    print(json.loads('41.87'))                           # json-number -> float
    print(json.loads('true'))                            # json-true   -> True
    print(json.loads('false'))                           # json-false  -> False
    print(json.loads('null'))                            # json-null   -> None

json_to_python()

Het programma heeft als output:

{'naam': 'Jan', 'leeftijd': 31}
['appel', 'banaan']
hallo
56
41.87
True
False
None

Inlezen json-file ( json.load )

Als input-file nemen we infile.json met de volgende inhoud:

[ {"naam": "Jan", "leeftijd": 31},
  ["appel", "banaan"],
  "hallo",
  56,
  41.87,
  true,
  false,
  null
]

Het Python-programma om het json-bestand in te lezen, ziet er als volgt uit:

import json

def read_json_file():
    with open("infile.json", mode="r", encoding="utf-8") as jsonfile:
        infile_data = json.load(jsonfile)
    print(infile_data)

read_json_file()

Als output krijgen we:

[{'naam': 'Jan', 'leeftijd': 31}, ['appel', 'banaan'], 
'hallo', 56, 41.87, True, False, None]

Wegschrijven json-file ( json.dump )

Een list wegschrijven als json-file
import json

def write_json_file():
    python_list = [{'naam': 'Jan', 'leeftijd': 31}, ['appel', 'banaan'], 
            'hallo', 56, 41.87, True, False, None]
    with open("outfile.json", mode="w", encoding="utf-8") as jsonfile:
        json.dump(python_list, jsonfile)

write_json_file()

De inhoud van het weggeschreven bestand is:

[{"naam": "Jan", "leeftijd": 31}, ["appel", "banaan"], "hallo", 
56, 41.87, true, false, null]
Een dictionary wegschrijven als json-file
import json

def write_json_file():
    python_dict = { 'naam': 'Jan',
                    'leeftijd': 31, 
                    'woonplaats': 'Rotterdam'
                   } 
    with open("outfile.json", mode="w", encoding="utf-8") as jsonfile:
        json.dump(python_dict, jsonfile)

write_json_file()
            

De inhoud van het weggeschreven bestand is:

{"naam": "Jan", "leeftijd": 31, "woonplaats": "Rotterdam"}

In dit voorbeeld werd een string als key gebruikt. Er zijn een drietal gegevenstypen die in Json niet als key mogen worden gebruikt. Dat zijn dict, list en tuple. Overzicht:

Python data type   Toegestaan als JSON key
dict 
list 
tuple 
str 
int 
float 
bool 
None 

Formatteren Json-code

Als input-file nemen we rechthoeken.json met de volgende inhoud:

[ { "illustration_upper_left_x": 0,
    "illustration_upper_left_y": 0,
    "element_x": 10,
    "element_y": 5, 
    "element_width": 300,
    "element_height": 30,
    "element_top": 10,
    "element_right": 10,
    "element_bottom": 10,
    "element_color": "lightGreen",
    "text_x": 15,
    "text_y": 24,
    "text_length": 100,
    "text": "the quick brown fox jumps over the lazy dog"
  },
  { "illustration_upper_left_x": 0,
    "illustration_upper_left_y": 0,
    "element_x": 10,
    "element_y": 5, 
    "element_width": 300,
    "element_height": 30,
    "element_top": 10,
    "element_right": 10,
    "element_bottom": 10,
    "element_color": "lightGreen",
    "text_x": 15,
    "text_y": 24,
    "text_length": 100,
    "text": "filmquiz bracht knappe ex-yogi van de wijs"
  },
  { "illustration_upper_left_x": 0,
    "illustration_upper_left_y": 0,
    "element_x": 10,
    "element_y": 5, 
    "element_width": 300,
    "element_height": 30,
    "element_top": 10,
    "element_right": 10,
    "element_bottom": 10,
    "element_color": "lightGreen",
    "text_x": 15,
    "text_y": 24,
    "text_length": 100,
    "text": { "text_1": "Als beginnend concertist debuteerde een ",
              "text_2": "fijngevoelige gitarist, hierna improviseerden",
              "text_3": "jeugdige klankkunstenaars levendig maar",
              "text_4": "notenblind op Peruviaanse quena's, robuuste",
              "text_5": "slagwerkers trommelden uitzinnige volksmuziek,",
              "text_6": "waarna xylofonisten 'Yesterday' zongen.; "
            }
  }
]

Het Python-programma om het json-bestand rechthoeken.json in te lezen, ziet er als volgt uit:

import json

def read_json_file():
    with open("rechthoeken.json", mode="r", encoding="utf-8") as jsonfile:
        infile_data = json.load(jsonfile)
    print(infile_data)

read_json_file()

De outputfile bestaat uit één regel waarin de gehele list is opgenomen.

[{'illustration_upper_left_x': 0, 'illustration_upper_left_y': 0, 'element_x': 10, 'element_y': 5, 'element_width': 300, 'element_height': 30, 'element_top': 10, 'element_right': 10, 'element_bottom': 10, 'element_color': 'lightGreen', 'text_x': 15, 'text_y': 24, 'text_length': 100, 'text': 'the quick brown fox jumps over the lazy dog'}, {'illustration_upper_left_x': 0, 'illustration_upper_left_y': 0, 'element_x': 10, 'element_y': 5, 'element_width': 300, 'element_height': 30, 'element_top': 10, 'element_right': 10, 'element_bottom': 10, 'element_color': 'lightGreen', 'text_x': 15, 'text_y': 24, 'text_length': 100, 'text': 'filmquiz bracht knappe ex-yogi van de wijs'}, {'illustration_upper_left_x': 0, 'illustration_upper_left_y': 0, 'element_x': 10, 'element_y': 5, 'element_width': 300, 'element_height': 30, 'element_top': 10, 'element_right': 10, 'element_bottom': 10, 'element_color': 'lightGreen', 'text_x': 15, 'text_y': 24, 'text_length': 100, 'text': {'text_1': 'Als beginnend concertist debuteerde een ', 'text_2': 'fijngevoelige gitarist, hierna improviseerden', 'text_3': 'jeugdige klankkunstenaars levendig maar', 'text_4': "notenblind op Peruviaanse quena's, robuuste", 'text_5': 'slagwerkers trommelden uitzinnige volksmuziek,', 'text_6': "waarna xylofonisten 'Yesterday' zongen.; "}}]

Als je binnen je programma een beter beeld wilt hebben van wat je hebt ingelezen, kun je de ingelezen data formatteren. Bijvoorbeeld:

import json

def read_json_file():
    with open("rechthoeken.json", mode="r", encoding="utf-8") as jsonfile:
        infile_data = json.load(jsonfile)
        formatted_data = json.dumps(infile_data, 
                                    indent=4, separators=(". ", " = "))
        print(formatted_data)
        
read_json_file()

De output wordt dan als volgt geformatteerd:

[
    {
        "illustration_upper_left_x" = 0. 
        "illustration_upper_left_y" = 0. 
        "element_x" = 10. 
        "element_y" = 5. 
        "element_width" = 300. 
        "element_height" = 30. 
        "element_top" = 10. 
        "element_right" = 10. 
        "element_bottom" = 10. 
        "element_color" = "lightGreen". 
        "text_x" = 15. 
        "text_y" = 24. 
        "text_length" = 100. 
        "text" = "the quick brown fox jumps over the lazy dog"
    }. 
    {
        "illustration_upper_left_x" = 0. 
        "illustration_upper_left_y" = 0. 
        "element_x" = 10. 
        "element_y" = 5. 
        "element_width" = 300. 
        "element_height" = 30. 
        "element_top" = 10. 
        "element_right" = 10. 
        "element_bottom" = 10. 
        "element_color" = "lightGreen". 
        "text_x" = 15. 
        "text_y" = 24. 
        "text_length" = 100. 
        "text" = "filmquiz bracht knappe ex-yogi van de wijs"
    }. 
    {
        "illustration_upper_left_x" = 0. 
        "illustration_upper_left_y" = 0. 
        "element_x" = 10. 
        "element_y" = 5. 
        "element_width" = 300. 
        "element_height" = 30. 
        "element_top" = 10. 
        "element_right" = 10. 
        "element_bottom" = 10. 
        "element_color" = "lightGreen". 
        "text_x" = 15. 
        "text_y" = 24. 
        "text_length" = 100. 
        "text" = {
            "text_1" = "Als beginnend concertist debuteerde een ". 
            "text_2" = "fijngevoelige gitarist, hierna improviseerden". 
            "text_3" = "jeugdige klankkunstenaars levendig maar". 
            "text_4" = "notenblind op Peruviaanse quena's, robuuste". 
            "text_5" = "slagwerkers trommelden uitzinnige volksmuziek,". 
            "text_6" = "waarna xylofonisten 'Yesterday' zongen.; "
        }
    }
]

Comprimeren

In Python creëren we een dictionary, die we op twee manieren omzetten naar json-formaat. De tweede manier bevat minder spaties, en is daardoor korter.

import json

def create_json():
    # creëer dictionary json_dict
    json_dict = { 1: { "illustration_upper_left_x": 0,
                           "illustration_upper_left_y": 0,
                          "element_x": 10,
                          "element_y": 5, 
                          "element_width": 300,
                          "element_height": 30,
                          "element_top": 10,
                          "element_right": 10,
                          "element_bottom": 10,
                          "element_color": "lightGreen",
                          "text_x": 15,
                          "text_y": 24,
                          "text_length": 100,
                          "text": "the quick brown fox jumps over the lazy dog"
                        },
                  2: { "illustration_upper_left_x": 0,
                          "illustration_upper_left_y": 0,
                          "element_x": 10,
                          "element_y": 5, 
                          "element_width": 300,
                          "element_height": 30,
                          "element_top": 10,
                          "element_right": 10,
                          "element_bottom": 10,
                          "element_color": "lightGreen",
                          "text_x": 15,
                          "text_y": 24,
                          "text_length": 100,
                          "text": "filmquiz bracht knappe ex-yogi van de wijs"
                        }
                  }
    # zet dictionary json_dict om in string json_data
    json_data = json.dumps(json_dict)
    print('aantal bytes in json_data :', len(json_data))

    # schrijf json_data weg als bestand outfile1.json
    with open("outfile1.json", mode="w", encoding="utf-8") as output_file:
        output_file.write(json_data)
        
    # zet dictionary json_dict om in string mini_json
    mini_json = json.dumps(json_dict, indent=None, separators=(",", ":"))
    print('aantal bytes in mini_json :', len(mini_json))
    
    # schrijf mini_jason weg als bestand outfile2.json
    with open("outfile2.json", mode="w", encoding="utf-8") as output_file:
        output_file.write(mini_json)

create_json()

De output is:.

aantal bytes in json_data : 687
aantal bytes in mini_json : 630

Een csv-bestand omzetten naar een json-bestand

De volgende python-code zet het csv-bestand 'infile.csv' om naar het json-bestand 'outfile.json':

import csv 
import json

def csv_to_json(csvFilePath, jsonFilePath):
    jsonArray = []
    try:
        with open(csvFilePath, encoding='utf-8') as csvf: 
            csvReader = csv.DictReader(csvf)
            for row in csvReader: 
                jsonArray.append(row)
                
        with open(jsonFilePath, 'w', encoding='utf-8') as jsonf: 
            jsonString = json.dumps(jsonArray, indent=4)
            jsonf.write(jsonString)
    except FileNotFoundError:
        print("csv_to_json: csv-input-file niet gevonden " + csvFilePath)

def main():
    csvFilePath = r'infile.csv'
    jsonFilePath = r'outfile.json'
    csv_to_json(csvFilePath, jsonFilePath)
         
if __name__ == '__main__':
    main()

Type hints

variabele

Een gewone manier om een variabele te definiëren is :

a = 15
print(a)

De output is:

15

Je kunt specificeren welk type de variabele moet hebben :

a: int = 15
print(a)

De output blijft:

15

Intern in Python wordt in de directory __annotations__ de type-hint opgeslagen:

a: int = 15
print(a)
print(__annotations__)

De output wordt:

15
{'a': <class 'int'>}

De python-interpreter doet helemaal niets met de toevoeging van ': int'. Het is meer bedoeld als herinnering voor de programmeur, in de trant van 'het is de bedoeling dat een variabele met de naam a van het type int is. ' Als een programmeur zich niet aan zo'n voornemen houdt, genereert de python-interpreter geen errors of warnings. Het volgende programma kent de waarde 15.72 van het type float toe aan variabele a, maar specificeert dat a een integer-waarde zou moeten hebben. Niettemin wordt er bij uitvoeriing van het programma geen waarschuwing gegenereerd:

a: int = 15.72
print(a)
print(__annotations__)

De output is:

15.72
{'a': <class 'int'>}

Toevoegingen als ': int' of ': float' worden 'type hints' genoemd Sommige editors (PyCharm) en programma's van externe partijen (MyPy) gebruiken type hints om inconsistenties in programma's op te sporen.

int, float, list, tuple, dict, bool, None
s: str = "abcd"
i: int = 15
f: float = 41.25
l: list = [ "a", "b" ]
t: tuple = ( "a", "b" )
d: dict = { 1 : "a" }
b: bool = True
n: None = None

print(s)
print(i)
print(f)
print(l)
print(t)
print(d)
print(b)
print(n)
print('-----')
print(__annotations__)

De output is:

abcd
15
41.25
['a', 'b']
('a', 'b')
{1: 'a'}
True
None
-----
{'s': <class 'str'>, 'i': <class 'int'>, 'f': <class 'float'>, 'l': <class 'list'>, 't': <class 'tuple'>, 'd': <class 'dict'>, 'b': <class 'bool'>, 'n': None}

In het volgende programma worden drie functies zonder type hints gedefinieerd:

def func_1(var): 
    return print(var)

def func_2(var = "x"): 
    return print(var)

def func_3(var = "y"):
    print(var) 
    return var

func_1("a")
func_2()
func_2("b")
func_3()
func_3("c")

Bij func_2 is de default-waarde voor variabele var gelijkgesteld aan "x". Bij func_3 is deze default-waarde gelijkgesteld aan "y". De output is:

a
x
b
y
c

Je kunt aan deze functies type-hints toevoegen De type-hint voor de return-waarde van de functie geeft je aan met '->':

def func_1(var: str): 
    return print(var)

def func_2(var: str = "x"): 
    return print(var)

def func_3(var: str = "y") -> str: 
    print(var) 
    return var
 
func_1("a")
func_2()
func_2("b")
func_3()
func_3("c")

print('-----')
print(__annotations__)
print(func_1.__annotations__)
print(func_2.__annotations__)
print(func_3.__annotations__)

De output wordt

a
x
b
y
c
-----
{}
{'var': <class 'str'>}
{'var': <class 'str'>}
{'var': <class 'str'>, 'return': <class 'str'>}

De volgende code definieert een list, een tuple en een dictionary:

namen = ["Jan", "Piet", "Kees"]
versies = (3, 7, 2)
opties = {"gecentreerd": False, "met_hoofdletters": True}

print(namen[0], namen[1], namen[2])
print(versies[0], versies[1], versies[2])
print(opties["gecentreerd"], opties["met_hoofdletters"])

De output is:

Jan Piet Kees
3 7 2
False True

Je kunt aan deze code type hints voor de individuele objecten in de list toevoegen. Je moet daarvoor wel delen van de standaard-module typing importeren.

from typing import Dict, List, Tuple

namen: List[str] = ["Jan", "Piet", "Kees"]
versies: Tuple[int, int, int] = (3, 7, 1)
opties: Dict[str, bool] = {"gecentreerd": False, "met_hoofdletters": True}

print(namen[0], namen[1], namen[2])
print(versies[0], versies[1], versies[2])
print(opties["gecentreerd"], opties["met_hoofdletters"])

print('-----')
print(__annotations__)

De output wordt:

Jan Piet Kees
3 7 2
False True
-----
{'namen': typing.List[str], 'versies': typing.Tuple[int, int, int], 'opties': typing.Dict[str, bool]}

We gaan uit van een programma waarin een class en een object worden gedefinieerd.

import math

class Mal():
    pi = math.pi

    def __init__(self, radius):
        self.radius = radius

    def omtrek_en_oppervlakte(self):
        self.omtrek = 2 * self.pi * self.radius
        self.oppervlakte = self.pi * self.radius * self.radius
        return self.omtrek, self.oppervlakte
        
def main():
    object_1 = Mal(10)
    (omtrek, oppervlakte) = object_1.omtrek_en_oppervlakte()
    print(omtrek, oppervlakte)

main()

De output wordt:

62.83185307179586 314.1592653589793
=====

Aan het programma voegen we type-hints toe.

import math

class Circle_template():
    print('----- Circle_template() ----- class definiton -----')
    pi: float = math.pi
    print('Circle_template: __annotations__ =', __annotations__)

    def __init__(self, radius):
        print('-----__init__(self, radius) ----- method')
        self.radius: float = radius
        print('__init__(self, radius): self.__annotations__ ==', self.__annotations__)

    def omtrek_en_oppervlakte(self):
        print('----- omtrek_en_oppervlakte(self) ----- method')
        self.omtrek: float = 2 * self.pi * self.radius
        self.oppervlakte: float = self.pi * self.radius * self.radius
        print('omtrek_en_oppervlakte(self): __annotations__ =', __annotations__)
        print('omtrek_en_oppervlakte(self): self.__annotations__ ==', self.__annotations__)
        return self.omtrek, self.oppervlakte
        
def main():
    print('----- main() ----- function')
    circle = Circle_template(10)
    print( 'main(): circle.__annotations__ ==',  circle.__annotations__)
    (omtrek, oppervlakte) =  circle.omtrek_en_oppervlakte()
    print(omtrek, oppervlakte)

print('----- program -----')
main()

De output wordt:

------ Circle_template() ----- class definiton -----
Circle_template: __annotations__ = {'pi': }
----- program -----
----- main() ----- function
-----__init__(self, radius) ----- method
__init__(self, radius): self.__annotations__ == {'pi': }
main(): circle.__annotations__ == {'pi': }
----- omtrek_en_oppervlakte(self) ----- method
omtrek_en_oppervlakte(self): __annotations__ = {}
omtrek_en_oppervlakte(self): self.__annotations__ == {'pi': }
62.83185307179586 314.1592653589793

Wat opvalt is dat omtrek en oppervlakte niet worden opgenomen in de annotations. Dit schijnt te maken te hebben met het feit dat een class een soort mal is, een template, dat als het gedefinieerd wordt, nog geen object is. Dit is voor mij vooralsnog onbekend terrein en ik ga er verder niet op in.

Refactoring

Refactoring gaat over het herschrijven van bestaande programmatuur. Het gaat erom om onduidelijke of onnodig gecompliceerde code om te zetten in goede code, zó, dat het programma precies blijft doen wat het altijd al deed. Waarom zou je bestaande programma's willen herschrijven? Daarvoor zijn verschillende redenen te bedenken:

In het boek 'Five lines of code' van Christian Clausen (Manning 2021) worden een aantal regels genoemd, die programma-code kunnen verbeteren. Cristian Clausen schrijft dat deze regels zijn bedoeld als zij-wieltjes. Met andere woorden: als hulpmiddel voor een beginner. Je mag dus afwijken van deze regels, als je daar goede redenen voor hebt. De regels zijn: Als je bestaande code wilt omzetten naar nieuwe volgens boovenstaande regels, heb je daarvoor zg. refactoring patterns:

Vijf statements in een functie

In het volgende programma voldoet de functie containsEven aan de regel, dat je maar vijf statements in een functie mag hebben, maar de functie minimum niet. De functie containsEven(arr) checkt of de twee-dimensionale array arr een even getal bevat. De functie minimum(arr) bepaalt het laagste getal in de twee-dimensionale array arr.

def containsEven(arr):
    for x in arr:
        for y in x:
            if y % 2 == 0:
                return True
    return False

def minimum(arr):
    r = (9**9)**9
    for x in arr:
        for y in x:
            if y < r:
                r = y
    return r
   
def main():
    arr = [ [21, 23, 67],
            [35, 67, 33],
            [41, 58, 89],
            [23, 11, 73],
          ]
    r = containsEven(arr)
    print(r)
    r = minimum(arr)
    print(r)

main()

De output wordt:

True
11

Als je er niet op let worden functies of methoden al maar langer. Daarmee wordt het lastiger ze te begrijpen.

Deel functies en methoden op in kleinere functies en methoden

Een hulpmiddel om te komen tot "five lines" is het refactoring-patroon "extract method. Deze passen we toe op bovenstaande functie minimum().

  1. Als een functie te lang is, probeer deze dan op te delen in stukken die bij elkaar horen.
  2. Scheidt die stukken door er blanco regels tussen te plaatsen. Eventueel kun je commentaar-regels tussenvoegen.
    In onderstaande programmacode wordt één stuk tussen twee commentaarregels geplaatst.
    def minimum(arr):
        r = (9**9)**9
        for x in arr:
            for y in x:
                #--------
                if y < r:
                    r = y
                #--------
        return r
       
    def main():
        arr = [ [21, 23, 67],
                [35, 67, 33],
                [41, 58, 89],
                [23, 11, 73],
              ]
        r = minimum(arr)
        print(r)
    
    main()
  3. Verplaats elk stuk naar een nieuwe functie (1).
    def minimum(arr):
        r = (9**9)**9
        for x in arr:
            for y in x:
                #--------
                #--------
        return r
    #--------
    def min():
        if y < r:
            r = y
    #--------  
    def main():
        arr = [ [21, 23, 67],
                [35, 67, 33],
                [41, 58, 89],
                [23, 11, 73],
              ]
        r = minimum(arr)
        print(r)
    
    main()
  4. Voeg parameters en return-waarden toe aan de functie. Roep de functie aan op de plaats waar de code zich oorspronkelijk bevond.
    def minimum(arr):
        r = (9**9)**9
        for x in arr:
            for y in x:
                #--------
                r = min(r, arr, x, y)
                #--------
        return r
    #--------
    def min(r, arr, x, y):
        if y < r:
            r = y
        return r
    #--------
    def main():
        arr = [ [21, 23, 67],
                [35, 67, 33],
                [41, 58, 89],
                [23, 11, 73],
              ]
        r = minimum(arr)
        print(r)
    
    main()
  5. Iedere keer als je een stukje code hebt opgenomen in een nieuwe functie, kijk je of er fouten in het programma zijn ontstaan, bijvoorbeeld door in IDLE te kiezen voor Run->Check Module (Alt-X). Bij een andere taal zou je kunnen compileren.
  6. Los de fouten die gerapporteerd worden op.
  7. Tenslotte verwijder je de blanco regels en het overbodig geworden commentaar.
    def minimum(arr):
        r = (9**9)**9
        for x in arr:
            for y in x:
                r = min(r, arr, x, y)
        return r
    
    def min(r, arr, x, y):
        if y < r:
            r = y
        return r
    
    def main():
        arr = [ [21, 23, 67],
                [35, 67, 33],
                [41, 58, 89],
                [23, 11, 73],
              ]
        r = minimum(arr)
        print(r)
    
    main()

ofwel een functie aanroepen, ofwel een functie meegeven als parameter, maar niet beide

Een korte vertaling van de regel "either call or pass' zou kunnen zijn: "ofwel aanroepen, ofwel doorgeven, maar niet beide". Een functie zou dus ofwel methoden behorende bij een object moeten aanroepen, ofwel het object doorgeven als een argument, maar niet beide. Als je het voorbeeld dat daarbij in het boek "Five lines of code" wordt gegeven, vertaalt naar Python, krijg je zoiets als

def average(arr):
    return sum(arr) / arr.__len__()

def main():
    arr = [21, 23, 67, 35, 67, 33,
           41, 58, 89, 23, 11, 84]
    print(average(arr))

main()

In sum(arr) wordt het object arr als argument doorgegeven aan de functie sum(), maar in arr.__len__() wordt een method van het object aangeroepen. In Python is dat nogal een gekunsteld voorbeeld, want arr.__len__() is een heel ongewone schrijfwijze van len(arr). Volgens de regel "either pass or call" zou je dit programma moeten verbeteren tot

def average(arr):
    return sum(arr) / len(arr)

def main():
    arr = [21, 23, 67, 35, 67, 33,
           41, 58, 89, 23, 11, 84]
    print(average(arr))

main()

De reden van de regel "either call or pass" wordt uitgelegd als: Elk statement in een functie zou hetzelfde abstractie-niveau moeten hebben. Het doorgeven van een object als argument leidt in de regel tot een hoger abstractie-niveau dan het aanroepen van een method van een object. Vandaar dat je binnen een functie moet kiezen voor het één of het ander.

Als in een functie/methode een if-statement voorkomt, moet dat if-statement het eerste statement van de functie zijn

De verschillende vertakkingen binnen een if-statement moet je zo veel mogelijk door afzonderlijke functie/methoden laten afhandelen. In de volgende functie zie je drie if-statements staan.

import math as m

def report_primes(n):
    for i in range(2, n):
        if is_prime(i):
            print(i)

def is_prime(i):
    s = m.floor(m.sqrt(i)) + 1
    has_factor = False
    for f in range(2, s):
        if i % f == 0:
            has_factor = True
    if has_factor ==  False:
        return True        

report_primes(100)

Als je het eerste if_statement vervangt door een aparte functie, krijg je

import math as m

def report_primes(n):
    for i in range(2, n):
        report_prime(i)

def report_prime(i):
        if is_prime(i):
            print(i)

def is_prime(i):
    s = m.floor(m.sqrt(i)) + 1
    has_no_factor = True
    for f in range(2, s):
        if i % f == 0:
            has_no_factor = False
    return has_no_factor

report_primes(100)

Als ik de andere if-statements in een aparte functie probeer onder te brengen, vind ik het programma er niet duidelijker op worden.

Vermijd elk else-statement

De regel luidt: Gebruik geen else in een if-statement, behalve wanneer we te maken hebben met een variabele waarvan we de inhoud niet kunnen bepalen (bijvoorbeeld het indrukken van toets). De reden om else zo min mogelijk te gebruiken is omdat het verwarrend kan zijn onder welke omstandigheden tot de else-vertakking wordt overgegaan. In onderstaand programma

def gemiddelde(getallen_reeks):
    if len(getallen_reeks) == 0:
        print('een lege getallenreeks is niet toegestaan')
    else:
        return sum(getallen_reeks) / len(getallen_reeks)

def main():
    print(gemiddelde([1, 2, 3]))
    print(gemiddelde([100, 200, 300]))
    print(gemiddelde([]))

if __name__ == '__main__':
    main()

vervangen we de vertakkingen van de if- en else-clausules door opeenvolgende functie-aanroepen. Elke van die functies begint met een if-statement zonder else-clausule.

def gemiddelde(getallen_reeks):
    check_getallen_reeks_leeg(getallen_reeks)
    return bereken_gemiddelde(getallen_reeks)

def check_getallen_reeks_leeg(getallen_reeks):
    if len(getallen_reeks) == 0:
        print('een lege getallenreeks is niet toegestaan')
        return None

def bereken_gemiddelde(getallen_reeks):
    if len(getallen_reeks) != 0:
        return sum(getallen_reeks) / len(getallen_reeks)

def main():
    print(gemiddelde([1, 2, 3]))
    print(gemiddelde([100, 200, 300]))
    print(gemiddelde([]))

if __name__ == '__main__':
    main()

De output van beide programma's is hetzelfde:

2.0
200.0
een lege getallenreeks is niet toegestaan
None

Als je deze werkwijze hanteert, wordt de programmatuur er dan duidelijker op? Als je niet in de gaten hebt, dat de functies check_getallen_reeks_leeg() en bereken_gemiddelde() beide met een if-statement beginnen, en de voorwaarden die bij die if-statements horen samen alle mogelijkheden vertegenwoordigen ( x == 0 en x != 0 vertegenwoordigen samen alle mogelijke waarden die x kan aannemen ), dan lijkt de inhoud van de functie gemiddelde() op een sequentie van een aantal acties.

Gebruik geen elif

Het boek 'Five lines of code' nodigt uit om onderscheid te maken tussen checks en decisions. Een if_statement is een check als er enkel gekeken wordt of er een bepaald iets aan de hand is. Een if-elif-statement kun soms meer opvatten als decision, als een beslissing die genomen moet worden. De filosofie is dat je beslissingen zo lang mogelijk moet uitstellen, dus zo laat mogelijk moet nemen. Om elif-clausules te vermijden propageert het boek 'Five lines of code' het refactoring-pattern 'Replace code with classes'.

Breng verschillende typen gegevens onder in classes

Als je in een if-elif-constructie te maken hebt met verschillende categorieën, bijvoorbeeld met rood, oranje en groen (bij stoplichten) of small, medium en large (bij kledingmaten), breng dan elk van die categorieën onder in een eigen class. Als voorbeeld nemen we het volgende programma:

def comment_bank(bank):
    if bank.upper() == 'ASN':
        print('ASN')
        print('duurzaam en geen wapenindustrie')
    elif bank.upper() == 'TRIODOS':
        print('TRIODOS')
        print('duurzaam')
    elif bank.upper() == 'ING':
        print('ING')
        print('voert oranje leeuw als mascotte')
    elif bank.upper() == 'RABO':
        print('RABO')
        print('was ooit coöperatief')
    elif bank.upper() == 'ABN':
        print('ABN')
        print('werd ooit opgekocht door de overheid')

def main():
    bank = input('Bank ')
    comment_bank(bank)

if __name__ == '__main__':
    main()

De gebruiker wordt gevraagd de naam van een bank in te voeren. Vervolgens wordt wat informatie over de bank getoond. De functie comment_bank() bevat een uitgebreid if-elif-else-statement. Dit programma gaan we in een aantal stappen herschrijven. Als een gebruiker ASN, Asn, asn of iets dergelijks intikt, verschijnt de tekst

ASN
duurzaam en geen wapenindustrie
Voor deze bank maken we een class, waarin wat de user specificeert (de naam van de bank) voorkomt als attribuut, en het resultaat (het tonen van informatie) voorkomt als methode. Je krijgt:
class Asn_bank():
    name = 'ASN' 
   
    def process_bank(self):
        print('ASN')
        print('duurzaam en geen wapenindustrie')

Iets soortgelijks doen we voor de andere banken die een gebruiker kan kiezen. We zorgen ervoor dat in de nieuwe classes het attribuut en de methode dezelfde naam hebben. We krijgen dan:

class Asn_bank():
    name = 'ASN'
    
    def process_bank(self):
        print('ASN')
        print('duurzaam en geen wapenindustrie')

class Triodos_bank():
    name = 'TRIODOS'

    def process_bank(self):
        print('TRIODOS')
        print('duurzaam')

class Ing_bank():
    name = 'ING'

    def process_bank(self):
        print('ING')
        print('voert oranje leeuw als mascotte')

class Rabo_bank():
    name = 'RABO'

    def process_bank(self):
        print('RABO')
        print('was ooit coöperatief')

class Abn_bank():
    name = 'ABN'

    def process_bank(self):
        print('ABN')
        print('werd ooit opgekocht door de overheid')

Elke class heeft een attribuut name en een methode process_bank(). Het programma begint met het maken van een object voor elke class.

    asn = Asn_bank()
    triodos = Triodos_bank()
    ing = Ing_bank()
    rabo = Rabo_bank()
    abn = Abn_bank()

We nemen deze objecten op in een list.

    bank_list = [asn, triodos, ing, rabo, abn]

Dit doen we, omdat we daarmee de verschillende classes één voor één kunnen benaderen. We vragen aan een gebruiker een banknaam in te tikken.

bank_input = input('Bank ')

Daarna doorlopen we de lijst bank-objecten. Als we bij de bank zijn aangekomen, die de gebruiker heeft gespecificeerd, voeren we de methode process_bank() uit.

Het programma ziet er in zijn geheel dan als volgt uit:

class Asn_bank():
    name = 'ASN'
    
    def process_bank(self):
        print('ASN')
        print('duurzaam en geen wapenindustrie')

class Triodos_bank():
    name = 'TRIODOS'

    def process_bank(self):
        print('TRIODOS')
        print('duurzaam')

class Ing_bank():
    name = 'ING'

    def process_bank(self):
        print('ING')
        print('voert oranje leeuw als mascotte')

class Rabo_bank():
    name = 'RABO'

    def process_bank(self):
        print('RABO')
        print('was ooit coöperatief')

class Abn_bank():
    name = 'ABN'

    def process_bank(self):
        print('ABN')
        print('werd ooit opgekocht door de overheid')

def main():

    # Maak bank-instellingen
    asn = Asn_bank()
    triodos = Triodos_bank()
    ing = Ing_bank()
    rabo = Rabo_bank()
    abn = Abn_bank()
    
    # Maak een lijst met banken
    bank_list = [asn, triodos, ing, rabo, abn]
       
    # Tik een bank in
    bank_input = input('Bank ')
    for bank in bank_list:
        if bank_input.strip().upper() == bank.name.upper():
            bank.process_bank()

if __name__ == '__main__':
    main()

We hebben nu het programma zodanig herschreven dat het if-elif-statement vervangen is door een if-statement zonder elif-clausules.
Als we gebruik maken van een dictionary in plaats van een list, kan het programma zonder if-statement worden geschreven.

class Asn_bank():
    
    def process_bank(self):
        print('ASN')
        print('duurzaam en geen wapenindustrie')

class Triodos_bank():
    
    def process_bank(self):
        print('TRIODOS')
        print('duurzaam')

class Ing_bank():

    def process_bank(self):
        print('ING')
        print('voert oranje leeuw als mascotte')

class Rabo_bank():

    def process_bank(self):
        print('RABO')
        print('was ooit coöperatief')

class Abn_bank():

    def process_bank(self):
        print('ABN')
        print('werd ooit opgekocht door de overheid')

def main():

    # Maak bank-instellingen
    asn = Asn_bank()
    triodos = Triodos_bank()
    ing = Ing_bank()
    rabo = Rabo_bank()
    abn = Abn_bank()
    
    # Maak dictionary met bank-instellingen

    bank_dict = {'ASN': asn,
                 'TRIODOS': triodos,
                 'ING': ing,
                 'RABO': rabo,
                 'ABN': abn
                }

    bank_input = input('bank ')
    try:
        bank_dict[bank_input.upper()].process_bank()
    except:
        pass

if __name__ == '__main__':
    main()

Breng code onder bij classes

Als voorbeeld nemen we het volgende programma:

def handle_event(invoer):
    print('handle_input(' + invoer + ')')
    if invoer == 'a':             
        print('naar links')
        move_horizontal(-1)
    elif invoer == 'd':
        print('naar rechts')
        move_horizontal(1)
    elif invoer == 'w':
        print('omhoog')
        move_vertical(-1)
    elif invoer == 's':
        print('omlaag')
        move_vertical(1)
    
def move_horizontal(x):
    print('move horizontal ' + str(x))
    
def move_vertical(y):
    print('move vertical ' + str(y))
    
def main():
    invoer = input('a=links, d=rechts, w=omhoog, s=omlaag ')
    handle_event(invoer)

if __name__ == '__main__':
    main()

Als een gebruiker een van de toetsen a, w, d of s indrukt, gaat er iets op het scherm naar links, naar boven , naar rechts of naar beneden. Het op en neer of omhoog en omlaag gaan wordt verzorgd door de functies move_vertical() en move_horizontal(). We willen in dit programma de functies move_horizontal() en move_vertical() dichterbij elkaar brengen. Daartoe creëren we voor elk van de vier mogelijkheden 'naar links', 'naar rechts, 'naar boven' en 'naar beneden' een class.

class Left(Invoer):

    def handle():
        move_horizontal(-1) 

class Right(Invoer):

    def handle():
        move_horizontal(1)

class Up(Invoer):

    def handle():
        move_vertical(-1)

class Down(Invoer):

    def handle():
        move_vertical(1)

Deze vier classes hebben dezelfde structuur. Merk op dat we bij bij de method handle() geen parameter self meegeven. Dat betekent dat het een class-method is. We hoeven geen gebruik te maken van objecten die zijn gebaseerd op Left, Right, Up en Down. De oorspronkelijke code herschrijven we nu.

class Left():

    def handle():
        move_horizontal(-1)
        
class Right():

    def handle():
        move_horizontal(1)
            
class Up():

    def handle():
        move_vertical(-1)
            
class Down():

    def handle():
        move_vertical(1)
            
def move_horizontal(x):
    print('move_horizontal(' + str(x) + ')')
    
def move_vertical(y):
    print('move_vertical(' + str(y) + ')')
    
def main():
    prompt = 'a=links, d=rechts, w=omhoog, s=omlaag '
    invoer = input(prompt)
    if invoer == 'a':
        invoer_class = Left
    elif invoer == 'd':
        invoer_class = Right
    elif invoer == 'w':
        invoer_class = Up
    elif invoer == 's':
        invoer_class = Down
    invoer_class.handle()

if __name__ == '__main__':
    main()

Als een gebruiker een keuze heeft gemaakt, wordt afhankelijk wat hij heeft ingetikt, in de variabele invoer_class een verwijzing gemaakt naar één van de classes Left, Right, Up or Down. Omdat in elk van die classes de method handle() een andere inhoud heeft, wordt steeds de bijbehorende move-actie uitgevoerd.

De code van een functie in een andere functie opnemen

In het volgende programma lijkt de functie rekening_bijwerken() overbodig.

class database:
    def update_rekening(rekening, bedrag):
        print('update_rekening :', bedrag, '->', rekening)
        
def rekening_bijwerken(rekening, bedrag):
    database.update_rekening(rekening, bedrag)

def overboeken(vanaf_rekening, naar_rekening, bedrag):
    rekening_bijwerken(vanaf_rekening, -bedrag)
    rekening_bijwerken(naar_rekening, bedrag)

def main():
    overboeken('ABN123', 'ING456', 543.21)
    
if __name__ == '__main__':
    main()

Je kunt de code van de functie rekening_bijwerken() direct opnemen in de functie overboeken().

class database:
    def update_rekening(rekening, bedrag):
        print('update_rekening :', bedrag, '->', rekening)
        
def overboeken(vanaf_rekening, naar_rekening, bedrag):
    database.update_rekening(vanaf_rekening, -bedrag)
    database.update_rekening(naar_rekening, bedrag)

def main():
    overboeken('ABN123', 'ING456', 543.21)
    
if __name__ == '__main__':
    main()

Splits een functie of method op in meerdere functies of methoden

Soms wordt de programmatuur begrijpelijker als een ingewikkelde functie wordt opgesplitst in functies die elk wat eenvoudiger te begrijpen zijn. In een spel kan een functie die antwoord geeft op de vraag 'welke vervolgacties zijn mogelijk?' soms beter gesplitst worden in bijvoorbeeld 'welke vervolgacties zijn mogelijk op de linkerflank?' en 'welke vervolgacties zijn mogelijk op de rechterflank?'.

Gebruik overerving alleen bij interfaces

Het woordenboek geeft als vertaling voor interface de woorden grensvlak, raakvlak en koppeling. In deze paragraaf gaat het over interfaces zoals die voorkomen in de programmeertaal Java. Interfaces in Java komen overeen met classes in Python, die als blauwdruk fungeren voor andere classes. Ze geven aan welke methods een class moet hebben, maar niet wat die methods moeten doen. Vertaald naar Python bevatten de methods van een interface enkel het statement pass. Volgens de regel 'Gebruik overerving alleen bij interfaces' moet je alleen overerving gebruiken als de superclass enkel de namen van de te definiëren methoden doorgeeft. Wat die methoden doen, moet je in de class zelf opgeven.
Overerving wordt vaak gebruikt om een default implementatie van een method te krijgen. De nadelen daarvan zijn vaak veel ingrijpender dan de voordelen. Code die door verschillende subklassen wordt gebruikt, veroorzaakt koppelingen.
Vrij vertaald betekent dit: Gebruik overerving zo min mogelijk; geef de vookeur aan composition.

Verwijder niet gebruikte functies en methods

Er zijn een aantal pakketten waarmee je functies en methodes die nergens worden aangeroepen in een Python-programma, kunt opsporen. Op internet vond ik (op 11-11-2024):

Voeg soortgelijke classes samen

Ik kan mij goed voorstellen dat er situaties zijn, dat een programma begrijpelijker en beter onderhoudbaar is, wanneer wordt onderkend dat een aantal classes beter samengevoegd kunnen worden tot één class. Maar het is mij niet gelukt om in Python van een werkend programma te verzinnen, dat eenvoudig en kort genoeg is, en niet gekunsteld, om als voorbeeld te dienen.

Vermijd identieke vertakkingen van if-statements

Het volgende code-fragment bevat twee vertakkingen waarop de code 'x = 0'volgt.

if a > 0:
    x = 0
if a == 0:
    x = 1
if a < 0:
    x = 0

Deze code kun je herschrijven tot

if a == 0:
    x = 1
else
    x = 0

of

x = 0
if a == 0:
    x = 1

Welke codering je voorkeur heeft is een beetje een kwestie van smaak. De volgende oplossing die 'or' gebruikt vind ik in dit kleine voorbeeld te omslachtig.

if a < 0 or a > 0:
    x = 0
if a == 0:
    x = 1

Het is van belang de if-statements zo begrijpelijk mogelijk te houden. Soms is het prettig een voorwaarde in een aparte functie te evalueren.

def check_perioden_overlappen_elkaar(
        periode_1_vanaf, periode_1_tm, periode_2_vanaf, periode_2_tm):
    if periode_2_tm < periode_1_vanaf or periode_1_tm < periode_2_vanaf:
        return False
    else:
        return True
    
def main():
    print('Overlappen twee perioden elkaar?')
    periode_1_vanaf = input('Eerste periode, vanaf-datum (JJJJMMDD) ')
    periode_1_tm = input('Eerste periode, t/m-datum   (JJJJMMDD) ' )
    periode_2_vanaf = input('Tweede periode, vanaf-datum (JJJJMMDD) ')
    periode_2_tm = input('Tweede periode, t/m-datum   (JJJJMMDD) ')
    print(' ')
    print('Eerste periode: ', periode_1_vanaf, 't/m', periode_1_tm)
    print('Tweede periode: ', periode_2_vanaf, 't/m', periode_2_tm)
    if check_perioden_overlappen_elkaar(
            periode_1_vanaf, periode_1_tm, periode_2_vanaf, periode_2_tm):
        print('De perioden overlappen elkaar')
    else:
        print('De perioden overlappen elkaar niet.')

if __name__ == '__main__':
    main()

Vermijd situaties met zij-effecten

Met 'situaties met zij-effecten' bedoelen we condities die waarden toekennen aan variabelen, foutsituaties genereren, iets afdrukken, iets naar een file wegschrijven, e.d.